package cc.shacocloud.mirage.loader.jar;

import cc.shacocloud.mirage.loader.data.RandomAccessData;
import cc.shacocloud.mirage.loader.data.RandomAccessDataFile;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.security.Permission;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Supplier;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;

/**
 * {@link java.util.jar.JarFile} 的扩展变体
 */
public class JarFile extends AbstractJarFile implements Iterable<java.util.jar.JarEntry> {
    
    private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
    
    private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
    
    private static final String HANDLERS_PACKAGE = "cc.shacocloud.mirage.loader.jar";
    
    private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
    
    private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
    
    private static final String READ_ACTION = "read";
    
    private final RandomAccessDataFile rootFile;
    
    private final String pathFromRoot;
    
    private final RandomAccessData data;
    
    private final JarFileType type;
    private final JarFileEntries entries;
    private final Supplier<Manifest> manifestSupplier;
    private URL url;
    private String urlString;
    private SoftReference<Manifest> manifest;
    
    private boolean signed;
    
    private String comment;
    
    private volatile boolean closed;
    
    private volatile JarFileWrapper wrapper;
    
    /**
     * 创建一个由指定文件支持的新 {@link JarFile}
     */
    public JarFile(File file) throws IOException {
        this(new RandomAccessDataFile(file));
    }
    
    /**
     * 创建一个由指定文件支持的新 {@link JarFile}
     */
    JarFile(RandomAccessDataFile file) throws IOException {
        this(file, "", file, JarFileType.DIRECT);
    }
    
    /**
     * 私有构造函数，用于直接或从嵌套条目创建新的 {@link JarFile}
     */
    private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarFileType type)
            throws IOException {
        this(rootFile, pathFromRoot, data, null, type, null);
    }
    
    private JarFile(@NotNull RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter,
                    JarFileType type, Supplier<Manifest> manifestSupplier) throws IOException {
        super(rootFile.getFile());
        if (System.getSecurityManager() == null) {
            super.close();
        }
        this.rootFile = rootFile;
        this.pathFromRoot = pathFromRoot;
        CentralDirectoryParser parser = new CentralDirectoryParser();
        this.entries = parser.addVisitor(new JarFileEntries(this, filter));
        this.type = type;
        parser.addVisitor(centralDirectoryVisitor());
        try {
            this.data = parser.parse(data, filter == null);
        } catch (RuntimeException ex) {
            try {
                this.rootFile.close();
                super.close();
            } catch (IOException ignore) {
            }
            throw ex;
        }
        this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> {
            try (InputStream inputStream = getInputStream(MANIFEST_NAME)) {
                if (inputStream == null) {
                    return null;
                }
                return new Manifest(inputStream);
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        };
    }
    
    /**
     * 注册一个 {@literal 'java.protocol.handler.pkgs'} 属性，以便找到 {@link URLStreamHandler} 来处理 jar URL。
     */
    public static void registerUrlProtocolHandler() {
        Handler.captureJarContextUrl();
        String handlers = System.getProperty(PROTOCOL_HANDLER, "");
        System.setProperty(PROTOCOL_HANDLER,
                ((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
        resetCachedUrlHandlers();
    }
    
    /**
     * 重置所有缓存的处理程序，以防已使用 jar 协，我们通过尝试设置空 {@link URLStreamHandlerFactory} 来重置处理程序，除了清除处理程序缓存外，它应该没有任何效果。
     */
    private static void resetCachedUrlHandlers() {
        try {
            URL.setURLStreamHandlerFactory(null);
        } catch (Error ignore) {
        }
    }
    
    @Contract(value = " -> new", pure = true)
    private @NotNull CentralDirectoryVisitor centralDirectoryVisitor() {
        return new CentralDirectoryVisitor() {
            
            @Override
            public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
                JarFile.this.comment = endRecord.getComment();
            }
            
            @Override
            public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) {
                AsciiBytes name = fileHeader.getName();
                if (name.startsWith(META_INF) && name.endsWith(SIGNATURE_FILE_EXTENSION)) {
                    JarFile.this.signed = true;
                }
            }
            
            @Override
            public void visitEnd() {
            }
            
        };
    }
    
    JarFileWrapper getWrapper() throws IOException {
        JarFileWrapper wrapper = this.wrapper;
        if (wrapper == null) {
            wrapper = new JarFileWrapper(this);
            this.wrapper = wrapper;
        }
        return wrapper;
    }
    
    @Override
    Permission getPermission() {
        return new FilePermission(this.rootFile.getFile().getPath(), READ_ACTION);
    }
    
    protected final RandomAccessDataFile getRootJarFile() {
        return this.rootFile;
    }
    
    RandomAccessData getData() {
        return this.data;
    }
    
    @Override
    public Manifest getManifest() throws IOException {
        Manifest manifest = (this.manifest != null) ? this.manifest.get() : null;
        if (manifest == null) {
            try {
                manifest = this.manifestSupplier.get();
            } catch (RuntimeException ex) {
                throw new IOException(ex);
            }
            this.manifest = new SoftReference<>(manifest);
        }
        return manifest;
    }
    
    @Override
    public Enumeration<java.util.jar.JarEntry> entries() {
        return new JarEntryEnumeration(this.entries.iterator());
    }
    
    @Override
    public Stream<java.util.jar.JarEntry> stream() {
        Spliterator<java.util.jar.JarEntry> spliterator = Spliterators.spliterator(iterator(), size(),
                Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL);
        return StreamSupport.stream(spliterator, false);
    }
    
    /**
     * 返回所包含条目的迭代器
     *
     * @see Iterable#iterator()
     */
    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Iterator<java.util.jar.JarEntry> iterator() {
        return (Iterator) this.entries.iterator(this::ensureOpen);
    }
    
    public JarEntry getJarEntry(CharSequence name) {
        return this.entries.getEntry(name);
    }
    
    @Override
    public JarEntry getJarEntry(String name) {
        return (JarEntry) getEntry(name);
    }
    
    public boolean containsEntry(String name) {
        return this.entries.containsEntry(name);
    }
    
    @Override
    public ZipEntry getEntry(String name) {
        ensureOpen();
        return this.entries.getEntry(name);
    }
    
    @Override
    InputStream getInputStream() throws IOException {
        return this.data.getInputStream();
    }
    
    @Override
    public synchronized InputStream getInputStream(ZipEntry entry) throws IOException {
        ensureOpen();
        if (entry instanceof JarEntry) {
            return this.entries.getInputStream((JarEntry) entry);
        }
        return getInputStream((entry != null) ? entry.getName() : null);
    }
    
    InputStream getInputStream(String name) throws IOException {
        return this.entries.getInputStream(name);
    }
    
    /**
     * 返回从指定条目加载的嵌套 {@link JarFile}
     */
    public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
        try {
            return createJarFileFromEntry(entry);
        } catch (Exception ex) {
            throw new IOException("无法打开嵌套的 jar 文件 '" + entry.getName() + "'", ex);
        }
    }
    
    private JarFile createJarFileFromEntry(@NotNull JarEntry entry) throws IOException {
        if (entry.isDirectory()) {
            return createJarFileFromDirectoryEntry(entry);
        }
        return createJarFileFromFileEntry(entry);
    }
    
    @Contract("_ -> new")
    private @NotNull JarFile createJarFileFromDirectoryEntry(@NotNull JarEntry entry) throws IOException {
        AsciiBytes name = entry.getAsciiBytesName();
        JarEntryFilter filter = (candidate) -> {
            if (candidate.startsWith(name) && !candidate.equals(name)) {
                return candidate.substring(name.length());
            }
            return null;
        };
        return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName().substring(0, name.length() - 1),
                this.data, filter, JarFileType.NESTED_DIRECTORY, this.manifestSupplier);
    }
    
    @Contract("_ -> new")
    private @NotNull JarFile createJarFileFromFileEntry(@NotNull JarEntry entry) throws IOException {
        if (entry.getMethod() != ZipEntry.STORED) {
            throw new IllegalStateException("无法打开嵌套条目 '" + entry.getName() + "'，它已被压缩，" +
                    "嵌套的jar文件必须在不压缩的情况下存储。请检查用于创建可执行 jar 文件的机制！");
        }
        RandomAccessData entryData = this.entries.getEntryData(entry.getName());
        return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData,
                JarFileType.NESTED_JAR);
    }
    
    @Override
    public String getComment() {
        ensureOpen();
        return this.comment;
    }
    
    @Override
    public int size() {
        ensureOpen();
        return this.entries.getSize();
    }
    
    @Override
    public void close() throws IOException {
        if (this.closed) {
            return;
        }
        super.close();
        if (this.type == JarFileType.DIRECT) {
            this.rootFile.close();
        }
        this.closed = true;
    }
    
    private void ensureOpen() {
        if (this.closed) {
            throw new IllegalStateException("jar 文件已关闭");
        }
    }
    
    boolean isClosed() {
        return this.closed;
    }
    
    String getUrlString() throws MalformedURLException {
        if (this.urlString == null) {
            this.urlString = getUrl().toString();
        }
        return this.urlString;
    }
    
    @Override
    public URL getUrl() throws MalformedURLException {
        if (this.url == null) {
            String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
            file = file.replace("file:////", "file://");
            this.url = new URL("jar", "", -1, file, new Handler(this));
        }
        return this.url;
    }
    
    @Override
    public String toString() {
        return getName();
    }
    
    @Override
    public String getName() {
        return this.rootFile.getFile() + this.pathFromRoot;
    }
    
    boolean isSigned() {
        return this.signed;
    }
    
    JarEntryCertification getCertification(JarEntry entry) {
        try {
            return this.entries.getCertification(entry);
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }
    
    public void clearCache() {
        this.entries.clearCache();
    }
    
    protected String getPathFromRoot() {
        return this.pathFromRoot;
    }
    
    @Override
    JarFileType getType() {
        return this.type;
    }
    
    /**
     * {@linkplain java.util.jar.JarEntry} 的 {@link Enumeration}
     */
    private static class JarEntryEnumeration implements Enumeration<java.util.jar.JarEntry> {
        
        private final Iterator<JarEntry> iterator;
        
        JarEntryEnumeration(Iterator<JarEntry> iterator) {
            this.iterator = iterator;
        }
        
        @Override
        public boolean hasMoreElements() {
            return this.iterator.hasNext();
        }
        
        @Override
        public java.util.jar.JarEntry nextElement() {
            return this.iterator.next();
        }
        
    }
    
}
